-
Notifications
You must be signed in to change notification settings - Fork 319
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Go to definition in classpath #23
Conversation
This commit makes it possible to quickly index a classpath of a project. This makes it possible to jump to definition from a project source file to a dependency source file. Note that this does not make is possible to jump to definition from dependency sources to dependency sources. While testing this feature, I extended the Jars utility to fetch -sources jars instead of regular jars and adapted the scala.meta.Classpath.deep method to read entries of jar files and report errors using our logging infrastructure.
The implementation in this commit is very hacky since I'm not sure how we can jump to definition inside entries of jars. I'm sure it's possible to just give the jar uri or something and everything automagically works. We index the entire classpath for every entry in the project/config matrix without any deduplication. This needs to be optimized. Most of the work was to get the compiler to correctly download the sources jars from the classpath. I had to wrap the call to `Jars.fetch` inside of a `Future` to unblock the compiler for future updates. This is a bug that needs to be fixed, we should not be randomly sprinkling blocks inside `Future { ... }`.
Previously we would re-index the same jars for every project/config and on every configuration change. Now we only index each jar once.
The positions are off and this implementation doesn't handle static methods.
It does not scale to have ctags indexing for all languages in a single file. The commit abstracts over the commonalities between scala and java indexing so that we can reuse most code between the two.
I've started getting MetaspaceErrors and I suspect they're related to us not properly cleaning up resources. Maybe this will fix the problem.
Go to definition for java sources in the classpath works! It doesn't handle all cases like static methods or inner classes. JDK sources are not indexed either, I think we have to special case it. However, this is a very promising start, as long as we can move the editor to a java source file we can delegate the rest to the Java LSP. I started getting MetaSpaceErrors, which is not good. The last commit adds one missing |
The JavaCtags module is quite primitive for now. I struggled to get nice positions for names, it seems they were all offsets. The infrastructure is there however to index java sources. In fact, we can add any other language for that matter! It should be possible for example to support go to definition in protobuf/thrift/avro sources. |
def index(filename: String, contents: String): Document = { | ||
val input = Input.VirtualFile(filename, contents) | ||
val tree = { | ||
import scala.meta._ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Lines 13-24 import various bits of scala.meta, then here you import all. Why? Is the style no wildcards allowed at top of file?
import java.nio.file.Files | ||
import java.util.Properties | ||
import java.util.concurrent.ConcurrentHashMap | ||
import java.util.concurrent.ConcurrentHashMap | ||
import java.util.concurrent.ConcurrentHashMap |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What caused this to be repeated 3 times? Some automated tool?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed with OrganizeImports
extends LazyLogging { | ||
private val documentPubSub = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This name makes me think it is (Publisher, Subscriber) but based on the usage it is actually (Subscriber, Publisher)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed.
Previous we could still overindex in race conditions, now we use Map.computeIfAbsent to properly make sure we only index each jar once. Race conditions are very possible since multiple .compilerconfig are often created at the same time.
package scala.meta.languageserver | ||
|
||
import java.io.File | ||
import java.nio.file.Files |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why are we using IO and NIO?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
java.io is only for File.pathSeparator
, otherwise we use nio for I/O work
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NIO Filesystem has a platform independent separator if you want to stick with the NIO api.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think I prefer java.io.File.pathSeparator over FileSystems.getDefault.getSeparator
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for the review @ShaneDelmore !
import java.nio.file.Files | ||
import java.util.Properties | ||
import java.util.concurrent.ConcurrentHashMap | ||
import java.util.concurrent.ConcurrentHashMap | ||
import java.util.concurrent.ConcurrentHashMap |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed with OrganizeImports
extends LazyLogging { | ||
private val documentPubSub = |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed.
package scala.meta.languageserver | ||
|
||
import java.io.File | ||
import java.nio.file.Files |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
java.io is only for File.pathSeparator
, otherwise we use nio for I/O work
) | ||
} | ||
|
||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
An alternative suggestion in a completely different style. Nothing wrong with this one though if you want to keep it. Ignore my lack of filename in the IllegalArgumentExceptions, I just whipped this up in a worksheet.
def pos(content: String, line: Int, col: Int): Int = {
val (lineCount, pos) = content
.linesWithSeparators
.take(line + 1)
.foldLeft((0, 0)){ (accum, str) =>
val (lineNum, charCount) = accum
if (lineNum < line) {
(lineNum + 1, charCount + str.length)
} else {
require(str.length >= col, "Line is not long enough")
(lineNum + 1, charCount + col)
}
}
require(lineCount == line + 1, "Not enough lines")
pos
}
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This method is on a fairly critical path since it's run for every start/end point during completions. foldLeft
+ tuples creates a lot of unnecessary allocations compared to the while loops.
However, I just found out that scala.meta.Input has an internal private lazy val cachedLineIndices: Array[Int]
which allows you to get the offset of a line in from an array lookup. I've refactored to use this instead of rolling our own.
@@ -27,6 +27,7 @@ object ScalametaEnrichments { | |||
} | |||
} | |||
implicit class XtensionInputLSP(val input: m.Input) extends AnyVal { | |||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Accidental new line?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed.
- static classes - static methods
- handle enums in java - reuse test suite for java/scala
* | ||
* Observable[Unit] is not descriptive of what the observable represents. | ||
* Instead, we create Unit-like types to better document what effects are | ||
* flowing through our application. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Love this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Poor man's type-safety. I refactored to this in fact because I hit on bugs related to accidentally creating Observable[Observable[T]]
from a .map
when I should have been using .flatMap
to build Observable[Unit]
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
are these phantom types?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not really since those values also exist at runtime. I'm still not sure if this was necessary, maybe we can avoid it by using stricter scalac options (no discard unit). However, I think it's a nice replacement for Observable[Unit]
.
The JDK needs to be special handled since it's included in the classpath by default and does not appear in the `updateClassifiers` report from sbt. The JDK on my machine contains ~2.5M loc but still indexes in ~8s at a staggering rate of ~450k loc/s. The need to persist these indices across editor sessions grows bigger.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I went through the whole PR and I think I understand the most of it. Very nice work @olafurpg!
I've left some comments and questions, but overall I think we can merge this and improve things in separate issues.
I think the greatest improvements (maybe for a future PR) would be:
- preload .compilerconfig files (easy)
- make the jump to sources less hacky (without copying the file). I don't fully understand what Java LSP does, but that seems LSP-compliant
def classloadScalafmt(version: String, out: PrintStream): Formatter = { | ||
val urls = Jars | ||
.fetch("com.geirsson", "scalafmt-cli_2.12", version, out) | ||
.iterator | ||
.map(_.toURI.toURL) | ||
.toArray | ||
out.println(s"Classloading scalafmt with ${urls.length} downloaded jars") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why are you printing to out
instead of using the logger?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because this is actually run before we redirect stdout, so if we use logger we it crashes the vscode client. However, I clearly should just redirect stdout before loading scalafmt 😅
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed, removed out
parameter.
def fetch( | ||
modules: Iterable[ModuleID], | ||
out: PrintStream, | ||
sources: Boolean |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
bikeshedding here, but sources
isn't very descriptive. How about: fetchSources
or shouldFetchSources
, or similar?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done, fetchSourceJars
.
logger.info(s"File $path changed, extension=$name") | ||
name match { | ||
case "semanticdb" => semanticdbSubscriber.onNext(path) | ||
case "compilerconfig" => compilerConfigSubscriber.onNext(path) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should we also preload all existing compilerconfig
files at launch, similarly to what we do with semanticdb
files?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We do actually, since we call this method from loadAllSemanticdbs... method, I've renamed the method to be more descriptive.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NB, we should really run that method asynchronously from the initialize method since it slows down server startup.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, in general I think initialize should return immediately and run the rest of the work asynchronously. We can even dynamically register for capabilities when the single services finish initializing.
def check(filename: String, original: String, expected: String): Unit = { | ||
test(filename) { | ||
val obtained = Ctags.index(filename, original) | ||
println(obtained) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
forgot a println
class ClasspathCtagsTest extends FunSuite with DiffAssertions { | ||
|
||
// NOTE(olafur) this test is a bit slow since it downloads jars from the internet. | ||
test("index classpath") { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
should we group this under a ci-slow
target to run separately (like we do in Scalafix?)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I like the idea of a CI slow category as well.
ctags.Ctags.index(sourcesClasspath) { doc => | ||
documentSubscriber.onNext(doc) | ||
} | ||
import scala.collection.JavaConverters._ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what is this import for?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's unused, removed.
sourceJars: List[AbsolutePath] | ||
) { | ||
override def toString: String = | ||
s"CompilerConfig(" + |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
since we have pprint as a dependency, you can probably just do pprint.stringify(this)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can't find pprint.stringify
, it seems to have been removed in 0.5. I don't think pprint includes the names of the fields. This string appears in the logs whenever a new presentation compiler is loaded so I customized it to make it compact and readable (full classpath can be huge).
MulticastStrategy.Publish, | ||
OverflowStrategy.ClearBuffer(2) | ||
) | ||
Observable.multicast[AbsolutePath](MulticastStrategy.Publish) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why is the overflow strategy gone?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I added it without knowing what it does. I think we should use the default unless we know what we're doing.
// However, that is a vscode only solution and we'd like this work for all | ||
// text editors. Therefore, we write instead the file contents to disk in order to | ||
// return a file: uri. | ||
private def createFileInWorkspaceTarget( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just as a comparison, the Java LSP jumps to a .class
file containing the source.
I'm not sure I understand the implementation, but here's the relevant bit https://github.com/eclipse/eclipse.jdt.ls/blob/2127f12fb3700dd783b75d973e4f2ecc134b9ddf/org.eclipse.jdt.ls.core/src/org/eclipse/jdt/ls/core/internal/handlers/NavigateToDefinitionHandler.java#L52
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice find! I opened #36 to track this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added note in code.
def index(input: Input.VirtualFile): Document = { | ||
logger.trace(s"Indexing ${input.path} with length ${input.value.length}") | ||
val indexer: CtagsIndexer = | ||
if (isScala(input.path)) ScalaCtags.index(input) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what would it take to index .sbt
files?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not much really, I can give some pointers if you open an issue 😉
- Move overly deeply nested methods defined inside indexRoot(). Diff is huge, but it's only indentation. - Catch NPE which happens in the JDK, don't want it to appear in the logs every time you start the language server.
Previously the logs contains a ton of "File ...*.class changed", which is not useful.
The handling of default fields was apparently wrong, so I skipped the tests. I also skip the slow tests that do heavy IO. We can consider adding a IntegrationTest or separate testsSlow project, but for now I want snappy tests and a small build.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thank you for the detailed review @gabro this PR became way too big, I kept adding more stuff to it while waiting for the lsp4j port. I will keep the PRs smaller from now on 😄
Some unattended changes
- slow tests, we ignore those for now. We can add a separate project or config later. I don't want to use scalatest
Slow
tags for this since I wantmetaserver/test
to be snappy by default. - jump to classpath, delegated to Figure out how to jump to files in zip/jar/.class files #36
build.sbt
Outdated
"io.get-coursier" %% "coursier" % coursier.util.Properties.version, | ||
"io.get-coursier" %% "coursier-cache" % coursier.util.Properties.version, | ||
"ch.epfl.scala" % "scalafix-cli" % "0.5.3" cross CrossVersion.full, | ||
"com.geirsson" %% "scalafmt-core" % "1.3.0" | ||
"org.scalatest" %% "scalatest" % "3.0.3" % "test", |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done, this was just copy pasted from above, I normally use Test
ctags.Ctags.index(sourcesClasspath) { doc => | ||
documentSubscriber.onNext(doc) | ||
} | ||
import scala.collection.JavaConverters._ |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's unused, removed.
sourceJars: List[AbsolutePath] | ||
) { | ||
override def toString: String = | ||
s"CompilerConfig(" + |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can't find pprint.stringify
, it seems to have been removed in 0.5. I don't think pprint includes the names of the fields. This string appears in the logs whenever a new presentation compiler is loaded so I customized it to make it compact and readable (full classpath can be huge).
* | ||
* Observable[Unit] is not descriptive of what the observable represents. | ||
* Instead, we create Unit-like types to better document what effects are | ||
* flowing through our application. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not really since those values also exist at runtime. I'm still not sure if this was necessary, maybe we can avoid it by using stricter scalac options (no discard unit). However, I think it's a nice replacement for Observable[Unit]
.
def classloadScalafmt(version: String, out: PrintStream): Formatter = { | ||
val urls = Jars | ||
.fetch("com.geirsson", "scalafmt-cli_2.12", version, out) | ||
.iterator | ||
.map(_.toURI.toURL) | ||
.toArray | ||
out.println(s"Classloading scalafmt with ${urls.length} downloaded jars") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because this is actually run before we redirect stdout, so if we use logger we it crashes the vscode client. However, I clearly should just redirect stdout before loading scalafmt 😅
logger.info(s"File $path changed, extension=$name") | ||
name match { | ||
case "semanticdb" => semanticdbSubscriber.onNext(path) | ||
case "compilerconfig" => compilerConfigSubscriber.onNext(path) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NB, we should really run that method asynchronously from the initialize method since it slows down server startup.
// However, that is a vscode only solution and we'd like this work for all | ||
// text editors. Therefore, we write instead the file contents to disk in order to | ||
// return a file: uri. | ||
private def createFileInWorkspaceTarget( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nice find! I opened #36 to track this.
// However, that is a vscode only solution and we'd like this work for all | ||
// text editors. Therefore, we write instead the file contents to disk in order to | ||
// return a file: uri. | ||
private def createFileInWorkspaceTarget( |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added note in code.
def index(input: Input.VirtualFile): Document = { | ||
logger.trace(s"Indexing ${input.path} with length ${input.value.length}") | ||
val indexer: CtagsIndexer = | ||
if (isScala(input.path)) ScalaCtags.index(input) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not much really, I can give some pointers if you open an issue 😉
|_root_.sourcecode.Util.getName. => def getName | ||
|_root_.sourcecode.Util.isSynthetic. => def isSynthetic | ||
|_root_.sourcecode.Util.isSyntheticName. => def isSyntheticName | ||
""".stripMargin |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
YES I WANT DIS IN SCALA 🙏
MERGING 🚀 |
#35 is probably the biggest priority right now, since as you may notice, starting the server will always trigger reindexing the full JDK, even for trivial projects. |
Does it happen on initialization or on the first definition request? |
I think when compilerconfig is detected, so on initialization |
It's triggered during initialization, but it does not block server initialize. |
OK |
object ScalaCtags { | ||
def index(input: Input.VirtualFile): CtagsIndexer = { | ||
val root: Source = input.parse[Source].get | ||
new Traverser with CtagsIndexer { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@olafurpg I was reading this code and I find it very similar to what I wanted to do for symbols outline. I started implementing something similar to this to traverse the tree just for the "global" symbols, but then I saw this and I'm wondering where is this Traverser
from and how to use it. Direct search in Scalameta repo didn't give me a definition (I'm completely lost in Scalameta's project structure 🙁 )
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@laughedelic I don’t have a computer in front of me to verify this but I believe some of the bits that you can’t tell where they are defined from scalameta are actually bits of the compiler plugin. Scala has a standard format for compiler plugins where they hook into a global content and supply their own traversed that the compiler uses to walk the ast and pull information out. It can appear a bit magic because it’s not fully scalameta code, but partially code for the plugin framework. I think traverser is one of these things.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
For example, see this scala plugin demo project. Every one I have seen folllows a similar structure. https://github.com/cb372/scalac-plugin-basic/blob/master/src/main/scala/basic/BasicPlugin.scala#L56
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In general, I think of scalameta as being intertwined with scalac so I don’t have to be, but when trying to understand scalameta source you will end up having to dig into scalac source as well as it is translating between the two ASTs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ShaneDelmore thanks a lot! This is quite enlightening. I didn't know anything about it. I'm not sure if I want to dig in the scalac's source just yet, but how can one use Scalameta without reading its sources? "_
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@laughedelic I think Traverser
is generated with macro annotations, it's essentially one big boilerplate pattern match on an ADT with 100s of nodes. Same for Transformer
. The Traverser
contract is quite simple however, override apply
, do somthing with the tree node, optionally continue visiting children with super.apply(tree)
.
.collect
is for example implemented on top of Traverser
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
scalameta doesn't convert between compiler data structures, the scalameta
module doesn't depend on scala-compiler in fact. However, scalameta (ab)uses macro annotations so a lot of the source code is hidden inside quasiquotes to be expanded at compile time. To understand the macros it's helpful to know a bit about scalac
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@laughedelic Here's how I picked up things about meta
- https://github.com/scalameta/scalameta/blob/23de1ca839e53ad6387981bd7cced35122a2cf76/scalameta/trees/shared/src/main/scala/scala/meta/Trees.scala
- http://scalameta.org/tutorial/#Trees
- a lot of exploration in the ammonite repl
@ import $ivy.`org.scalameta::contrib:2.0.0`, scala.meta._, contrib._
import $ivy.$ , scala.meta._, contrib._
@ q"trait A { val x: Int }"
res1: Defn.Trait = Defn.Trait(
List(),
Type.Name("A"),
List(),
Ctor.Primary(List(), _, List()),
Template(List(), List(), Self(_, None), List(Decl.Val(List(), List(Pat.Var(Term.Name("x"))), Type.Name("Int"))))
)
writing new docs on more detailed parts like Transformer
/Traverser
is on my TODO!
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@olafurpg hey! thanks a lot for explaining the things. I'm using the same "exploratory" approach too, it's quite enough for simple uses.
I just often wonder what is available in general (not to reinvent the wheel): normally if there's a generated scaladoc, it may be enough or (more often) I just check the sources and see the "global symbols outline" 😄 but with this "macrofull" code it's kind of hard (I'm also very superficially familiar with macros).
Another thing that bothers me is that I don't understand how code is organized in the scalameta repo. Probably there is some development dcoumentation for it that I didn't find?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ShaneDelmore btw, thanks for mentioning quasiquotes.md
, I didn't notice it before 👍 (I had Trees.scala
open instead 😅 )
case t: Defn.Object => term(t.name, OBJECT); Continue | ||
case t: Defn.Def => term(t.name, DEF); Stop | ||
case Defn.Val(_, Pat.Var(name) :: Nil, _, _) => term(name, DEF); Stop | ||
case Defn.Var(_, Pat.Var(name) :: Nil, _, _) => term(name, DEF); Stop |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@olafurpg What about declarations? For example if we have somewhere defined
trait T {
type X
val x: X
}
X
will be a Decl.Type
and x
a Decl.Val
, so if later in the code we encounter something like def foo(t: T): t.X
and want to lookup that t.X
... No, this example is bad, but I think you got the idea. If not, I'll think of a better example, sorry 😅
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Those are actually not handled! This is a bug 😅
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, I'll try to add it then.
Another question here: as I see from the definition of those term
/type
indexer methods they just use the name position, but for the outline we need the definition position instead. Do you think it could be also useful for the general indexer to use that wider position range? e.g.
case t: Defn.Def => term(t.name, t.pos, DEF)
Symbols outline would be then just a simple reindexing of the document on every documentSymbol
request.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you think it could be also useful for the general indexer to use that wider position range? e.g.
Yes! I've been considering refactoring the indexer to construct a custom data structure that we can tweak for our needs. Then we can reuse it for multiple features like outline + go to definition.
Can you open a ticket to discuss details?
This PR implements a
Ctags
module that is able to build semanticdbs for Scala and Java source files in the classpath, without any compilation or analyzing classfiles. The scala indexer uses the scalameta parser to find definitions of global symbols and record the positions at the rate of ~30k lines/s (see scalameta/scalameta#1130 for idea how to speed parsing by up to 40%). The Java indexer uses qdox which goes through java sources at ~250k loc/s on my laptop. qdox specifically designed for performance in mind and does not even parse bodies of methods or other metadata that scalameta parser collects.We store the full file contents of all source files in the classpath in-memory. This goes against one the the core goals of this project, which is to keep memory usage as low as possible.